﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace System
{
    /// <summary>
    /// Extension methods for String class.
    /// </summary>
    public static class StringExtensions
    {
        /// <summary>
        /// Adds a char to end of given string if it does not ends with the char.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="c"></param>
        /// <param name="comparisonType"></param>
        /// <returns></returns>
        public static string EnsureEndsWith(this string str, char c, StringComparison comparisonType = StringComparison.Ordinal)
        {
            if (str.EndsWith(c.ToString(), comparisonType))
            {
                return str;
            }

            return str + c;
        }

        /// <summary>
        /// Adds a char to beginning of given string if it does not starts with the char.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="c"></param>
        /// <param name="comparisonType"></param>
        /// <returns></returns>
        public static string EnsureStartsWith(this string str, char c, StringComparison comparisonType = StringComparison.Ordinal)
        {
            if (str.StartsWith(c.ToString(), comparisonType))
            {
                return str;
            }

            return c + str;
        }

        /// <summary>
        /// Indicates whether this string is null or an System.String.Empty string.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static bool IsNullOrEmpty(this string str)
        {
            return string.IsNullOrEmpty(str);
        }

        /// <summary>
        /// indicates whether this string is null, empty, or consists only of white-space characters.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static bool IsNullOrWhiteSpace(this string str)
        {
            return string.IsNullOrWhiteSpace(str);
        }

        /// <summary>
        /// Gets a substring of a string from beginning of the string.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="len"></param>
        /// <exception cref="ArgumentException">Thrown if len is bigger that string's length</exception>
        /// <exception cref="ArgumentNullException">Thrown if str is null</exception>
        /// <returns></returns> 
        public static string Left(this string str, int len)
        {
            if (str.Length < len)
            {
                throw new ArgumentException("len argument can not be bigger than given string's length!");
            }

            return str.Substring(0, len);
        }

        /// <summary>
        /// Converts line endings in the string to System.Environment.NewLine.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static string NormalizeLineEndings(this string str)
        {
            return str.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", Environment.NewLine);
        }

        /// <summary>
        /// Gets index of nth occurrence of a char in a string.
        /// </summary>
        /// <param name="str">source string to be searched</param>
        /// <param name="c">Char to search in str</param>
        /// <param name="n">Count of the occurrence</param>
        /// <returns></returns>
        public static int NthIndexOf(this string str, char c, int n)
        {
            int num = 0;
            for (int i = 0; i < str.Length; i++)
            {
                if (str[i] == c && ++num == n)
                {
                    return i;
                }
            }

            return -1;
        }

        /// <summary>
        /// Removes first occurrence of the given postfixes from end of the given string.
        /// </summary>
        /// <param name="str">The string.</param>
        /// <param name="postFixes">one or more postfix.</param>
        /// <returns>Modified string or the same string if it has not any of given postfixes</returns>
        public static string RemovePostFix(this string str, params string[] postFixes)
        {
            return str.RemovePostFix(StringComparison.Ordinal, postFixes);
        }

        /// <summary>
        /// Removes first occurrence of the given postfixes from end of the given string.
        /// </summary>
        /// <param name="str">The string.</param>
        /// <param name="comparisonType">String comparison type</param>
        /// <param name="postFixes">one or more postfix.</param>
        /// <returns>Modified string or the same string if it has not any of given postfixes</returns>
        public static string RemovePostFix(this string str, StringComparison comparisonType, params string[] postFixes)
        {
            if (str.IsNullOrEmpty())
            {
                return str;
            }

            if (postFixes.IsNullOrEmpty())
            {
                return str;
            }

            foreach (string text in postFixes)
            {
                if (str.EndsWith(text, comparisonType))
                {
                    return str.Left(str.Length - text.Length);
                }
            }

            return str;
        }

        /// <summary>
        /// Removes first occurrence of the given prefixes from beginning of the given string.
        /// </summary>
        /// <param name="str">The string.</param>
        /// <param name="preFixes">one or more prefix.</param>
        /// <returns>Modified string or the same string if it has not any of given prefixes</returns>   
        public static string RemovePreFix(this string str, params string[] preFixes)
        {
            return str.RemovePreFix(StringComparison.Ordinal, preFixes);
        }

        /// <summary>
        /// Removes first occurrence of the given prefixes from beginning of the given string.
        /// </summary>
        /// <param name="str">The string.</param>
        /// <param name="comparisonType">String comparison type</param>
        /// <param name="preFixes">one or more prefix.</param>
        /// <returns>Modified string or the same string if it has not any of given prefixes</returns>
        public static string RemovePreFix(this string str, StringComparison comparisonType, params string[] preFixes)
        {
            if (str.IsNullOrEmpty())
            {
                return str;
            }

            if (preFixes.IsNullOrEmpty())
            {
                return str;
            }

            foreach (string text in preFixes)
            {
                if (str.StartsWith(text, comparisonType))
                {
                    return str.Right(str.Length - text.Length);
                }
            }

            return str;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="str"></param>
        /// <param name="search"></param>
        /// <param name="replace"></param>
        /// <param name="comparisonType"></param>
        /// <returns></returns>
        public static string ReplaceFirst(this string str, string search, string replace, StringComparison comparisonType = StringComparison.Ordinal)
        {
            int num = str.IndexOf(search, comparisonType);
            if (num < 0)
            {
                return str;
            }

            return str.Substring(0, num) + replace + str.Substring(num + search.Length);
        }

        /// <summary>
        /// Gets a substring of a string from end of the string.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="len"></param>
        /// <exception cref="ArgumentNullException">Thrown if str is null</exception>
        /// <exception cref="ArgumentException">Thrown if len is bigger that string's length</exception>
        /// <returns></returns>
        public static string Right(this string str, int len)
        {
            if (str.Length < len)
            {
                throw new ArgumentException("len argument can not be bigger than given string's length!");
            }

            return str.Substring(str.Length - len, len);
        }

        /// <summary>
        /// Uses string.Split method to split given string by given separator.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="separator"></param>
        /// <returns></returns>
        public static string[] Split(this string str, string separator)
        {
            return str.Split(new string[1]
            {
                separator
            }, StringSplitOptions.None);
        }

        /// <summary>
        /// Uses string.Split method to split given string by given separator.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="separator"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        public static string[] Split(this string str, string separator, StringSplitOptions options)
        {
            return str.Split(new string[1]
            {
                separator
            }, options);
        }

        /// <summary>
        /// Uses string.Split method to split given string by System.Environment.NewLine.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static string[] SplitToLines(this string str)
        {
            return str.Split(Environment.NewLine);
        }

        /// <summary>
        /// Uses string.Split method to split given string by System.Environment.NewLine.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        public static string[] SplitToLines(this string str, StringSplitOptions options)
        {
            return str.Split(Environment.NewLine, options);
        }

        /// <summary>
        /// Converts PascalCase string to camelCase string.
        /// </summary>
        /// <param name="str">String to convert</param>
        /// <param name="useCurrentCulture">set true to use current culture. Otherwise, invariant culture will be used.</param>
        /// <returns>camelCase of the string</returns>
        public static string ToCamelCase(this string str, bool useCurrentCulture = false)
        {
            if (string.IsNullOrWhiteSpace(str))
            {
                return str;
            }

            if (str.Length == 1)
            {
                return useCurrentCulture ? str.ToLower() : str.ToLowerInvariant();
            }

            return (useCurrentCulture ? char.ToLower(str[0]) : char.ToLowerInvariant(str[0])) + str.Substring(1);
        }

        /// <summary>
        /// Converts given PascalCase/camelCase string to sentence (by splitting words by
        /// space). Example: "ThisIsSampleSentence" is converted to "This is a sample sentence".
        /// </summary>
        /// <param name="str">String to convert.</param>
        /// <param name="useCurrentCulture">set true to use current culture. Otherwise, invariant culture will be used.</param>
        /// <returns></returns>  
        public static string ToSentenceCase(this string str, bool useCurrentCulture = false)
        {
            if (string.IsNullOrWhiteSpace(str))
            {
                return str;
            }

            return useCurrentCulture ? Regex.Replace(str, "[a-z][A-Z]", (Match m) => m.Value[0] + " " + char.ToLower(m.Value[1])) : Regex.Replace(str, "[a-z][A-Z]", (Match m) => m.Value[0] + " " + char.ToLowerInvariant(m.Value[1]));
        }

        /// <summary>
        /// Converts given PascalCase/camelCase string to kebab-case.
        /// </summary>
        /// <param name="str">String to convert.</param>
        /// <param name="useCurrentCulture">set true to use current culture. Otherwise, invariant culture will be used.</param>
        /// <returns></returns>
        public static string ToKebabCase(this string str, bool useCurrentCulture = false)
        {
            if (string.IsNullOrWhiteSpace(str))
            {
                return str;
            }

            str = str.ToCamelCase();
            return useCurrentCulture ? Regex.Replace(str, "[a-z][A-Z]", (Match m) => m.Value[0] + "-" + char.ToLower(m.Value[1])) : Regex.Replace(str, "[a-z][A-Z]", (Match m) => m.Value[0] + "-" + char.ToLowerInvariant(m.Value[1]));
        }

        /// <summary>
        /// Converts given PascalCase/camelCase string to snake case. Example: "ThisIsSampleSentence"
        /// is converted to "this_is_a_sample_sentence". https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs#L51
        /// </summary>
        /// <param name="str">String to convert.</param>
        /// <returns></returns>
        public static string ToSnakeCase(this string str)
        {
            if (string.IsNullOrWhiteSpace(str))
            {
                return str;
            }

            StringBuilder stringBuilder = new StringBuilder(str.Length + Math.Min(2, str.Length / 5));
            UnicodeCategory? unicodeCategory = null;
            for (int i = 0; i < str.Length; i++)
            {
                char c = str[i];
                if (c == '_')
                {
                    stringBuilder.Append('_');
                    unicodeCategory = null;
                    continue;
                }

                UnicodeCategory unicodeCategory2 = char.GetUnicodeCategory(c);
                switch (unicodeCategory2)
                {
                    case UnicodeCategory.UppercaseLetter:
                    case UnicodeCategory.TitlecaseLetter:
                        if (unicodeCategory == UnicodeCategory.SpaceSeparator || unicodeCategory == UnicodeCategory.LowercaseLetter || (unicodeCategory != UnicodeCategory.DecimalDigitNumber && unicodeCategory.HasValue && i > 0 && i + 1 < str.Length && char.IsLower(str[i + 1])))
                        {
                            stringBuilder.Append('_');
                        }

                        c = char.ToLower(c);
                        break;
                    case UnicodeCategory.LowercaseLetter:
                    case UnicodeCategory.DecimalDigitNumber:
                        if (unicodeCategory == UnicodeCategory.SpaceSeparator)
                        {
                            stringBuilder.Append('_');
                        }

                        break;
                    default:
                        if (unicodeCategory.HasValue)
                        {
                            unicodeCategory = UnicodeCategory.SpaceSeparator;
                        }

                        continue;
                }

                stringBuilder.Append(c);
                unicodeCategory = unicodeCategory2;
            }

            return stringBuilder.ToString();
        }

        /// <summary>
        /// Converts string to enum value.
        /// </summary>
        /// <typeparam name="T">Type of enum</typeparam>
        /// <param name="value">String value to convert</param>
        /// <returns>Returns enum object</returns>
        public static T ToEnum<T>(this string value) where T : struct
        {
            return (T)Enum.Parse(typeof(T), value);
        }

        /// <summary>
        /// Converts string to enum value.
        /// </summary>
        /// <typeparam name="T">Type of enum</typeparam>
        /// <param name="value">String value to convert</param>
        /// <param name="ignoreCase">Ignore case</param>
        /// <returns>Returns enum object</returns>
        public static T ToEnum<T>(this string value, bool ignoreCase) where T : struct
        {
            return (T)Enum.Parse(typeof(T), value, ignoreCase);
        }

        /// <summary>
        /// 转md5
        /// </summary>
        /// <param name="str">String to Md5</param>
        /// <returns></returns>
        public static string ToMd5(this string str)
        {
            using (MD5 mD = MD5.Create())
            {
                byte[] bytes = Encoding.UTF8.GetBytes(str);
                byte[] array = mD.ComputeHash(bytes);
                StringBuilder stringBuilder = new StringBuilder();
                byte[] array2 = array;
                foreach (byte b in array2)
                {
                    stringBuilder.Append(b.ToString("X2"));
                }

                return stringBuilder.ToString();
            }
        }

        /// <summary>
        /// Converts camelCase string to PascalCase string.
        /// </summary>
        /// <param name="str">String to convert</param>
        /// <param name="useCurrentCulture">set true to use current culture. Otherwise, invariant culture will be used.</param>
        /// <returns>PascalCase of the string</returns>
        public static string ToPascalCase(this string str, bool useCurrentCulture = false)
        {
            if (string.IsNullOrWhiteSpace(str))
            {
                return str;
            }

            if (str.Length == 1)
            {
                return useCurrentCulture ? str.ToUpper() : str.ToUpperInvariant();
            }

            return (useCurrentCulture ? char.ToUpper(str[0]) : char.ToUpperInvariant(str[0])) + str.Substring(1);
        }

        /// <summary>
        /// Gets a substring of a string from beginning of the string if it exceeds maximum length.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="maxLength"></param>
        /// <exception cref="ArgumentNullException">Thrown if str is null</exception>
        /// <returns></returns>
        public static string Truncate(this string str, int maxLength)
        {
            if (str == null)
            {
                return null;
            }

            if (str.Length <= maxLength)
            {
                return str;
            }

            return str.Left(maxLength);
        }

        /// <summary>
        /// Gets a substring of a string from Ending of the string if it exceeds maximum length.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="maxLength"></param>
        /// <exception cref="ArgumentNullException">Thrown if str is null</exception>
        /// <returns></returns>
        public static string TruncateFromBeginning(this string str, int maxLength)
        {
            if (str == null)
            {
                return null;
            }

            if (str.Length <= maxLength)
            {
                return str;
            }

            return str.Right(maxLength);
        }

        /// <summary>
        /// Gets a substring of a string from beginning of the string if it exceeds maximum
        /// length. It adds a "..." postfix to end of the string if it's truncated. Returning
        /// string can not be longer than maxLength.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="maxLength"></param>
        /// <exception cref="ArgumentNullException">Thrown if str is null</exception>
        /// <returns></returns>
        public static string TruncateWithPostfix(this string str, int maxLength)
        {
            return str.TruncateWithPostfix(maxLength, "...");
        }

        /// <summary>
        /// Gets a substring of a string from beginning of the string if it exceeds maximum
        /// length. It adds given postfix to end of the string if it's truncated. Returning
        /// string can not be longer than maxLength.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="maxLength"></param>
        /// <param name="postfix"></param>
        /// <exception cref="ArgumentNullException">Thrown if str is null</exception>
        /// <returns></returns>
        public static string TruncateWithPostfix(this string str, int maxLength, string postfix)
        {
            if (str == null)
            {
                return null;
            }

            if (str == string.Empty || maxLength == 0)
            {
                return string.Empty;
            }

            if (str.Length <= maxLength)
            {
                return str;
            }

            if (maxLength <= postfix.Length)
            {
                return postfix.Left(maxLength);
            }

            return str.Left(maxLength - postfix.Length) + postfix;
        }

        /// <summary>
        /// Converts given string to a byte array using System.Text.Encoding.UTF8 encoding.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static byte[] GetBytes(this string str)
        {
            return str.GetBytes(Encoding.UTF8);
        }

        /// <summary>
        /// Converts given string to a byte array using the given encoding
        /// </summary>
        /// <param name="str"></param>
        /// <param name="encoding"></param>
        /// <returns></returns>
        public static byte[] GetBytes(this string str, Encoding encoding)
        {
            return encoding.GetBytes(str);
        }

        /// <summary>
        /// 判断给定的字符串是否为数字，允许出现+(加号)、－(减号)、.(小数点)
        /// </summary>
        /// <param name="str">给定的字符串</param>
        /// <returns></returns>
        public static bool IsNumeric(this string str)
        {
            //用IsDigit判断会合适些
            //(char.IsDigit(expr[0]))

            Dictionary<char, int> records = new Dictionary<char, int>();
            for (var i = 0; i < str.Length; i++)
            {
                char c = str[i];
                if (!(c >= '0' && c <= '9'))
                {
                    if (c == '+' || c == '-' || c == '.')
                    {
                        if (records.ContainsKey(c))
                        {
                            return false;
                        }
                        else
                        {
                            records[c] = 1;
                        }

                        //+和-号，只能出现在索引0位置
                        if (c == '+' || c == '-')
                        {
                            if (i > 0)
                            {
                                return false;
                            }
                        }
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// 判断字符串是为xml格式
        /// </summary>
        /// <param name="str">待判定的字符串</param>
        /// <returns></returns>
        public static bool IsXml(this string str)
        {
            try
            {
                XDocument.Parse(str);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// 将xml字符串格式化为美观的字符串
        /// </summary>
        /// <param name="xml">待格式化的xml字符串</param>
        /// <returns></returns>
        public static string FormatXml(this string xml)
        {
            XDocument doc = XDocument.Parse(xml);
            return doc.ToString();
        }

        /// <summary>
        /// 使用utf8字符编码将字符串转为Base64字符串
        /// </summary>
        /// <param name="input">待转为Base64字符串的原始字符串</param>
        /// <returns></returns>
        public static string ToBase64(this string input)
        {
            return input.ToBase64(Encoding.UTF8);
        }

        /// <summary>
        /// 使用指定的字符编码<paramref name="encoding"/>将字符串转为Base64字符串
        /// </summary>
        /// <param name="input">待转为Base64字符串的原始字符串</param>
        /// <param name="encoding">字符编码，用于将原始字符串转为指定编码的字节数组</param>
        /// <returns></returns>
        public static string ToBase64(this string input, Encoding encoding)
        {
            byte[] bytes = encoding.GetBytes(input);
            return Convert.ToBase64String(bytes);
        }

        /// <summary>
        /// 将Base64字符串使用utf8字符编码进行解码
        /// </summary>
        /// <param name="base64String">待解码的Base64字符串</param>
        /// <returns></returns>
        public static string FromBase64(this string base64String)
        {
            return base64String.FromBase64(Encoding.UTF8);
        }

        /// <summary>
        /// 将Base64字符串使用指定的字符编码<paramref name="encoding"/>进行解码
        /// </summary>
        /// <param name="base64String">待解码的Base64字符串</param>
        /// <param name="encoding">字符编码，用于将Base64字符串转为指定编码的字节数组</param>
        /// <returns></returns>
        public static string FromBase64(this string base64String, Encoding encoding)
        {
            byte[] bytes = Convert.FromBase64String(base64String);

            return encoding.GetString(bytes);
        }
    }
}
